/*******************************************************************************
 * Copyright (c) 2011 Andrey Loskutov.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the BSD License
 * which accompanies this distribution, and is available at
 * http://www.opensource.org/licenses/bsd-license.php
 * Contributor:  Andrey Loskutov - initial API and implementation
 *******************************************************************************/
package de.loskutov.bco.asm;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

import org.objectweb.asm.Attribute;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.util.ASMifier;
import org.objectweb.asm.util.TraceMethodVisitor;

import de.loskutov.bco.preferences.BCOConstants;

/**
 * @author Andrei
 */
public class CommentedASMifierClassVisitor extends ASMifier implements ICommentedClassVisitor {

    protected final boolean showLines;
    protected final boolean showLocals;
    protected final boolean showStackMap;
    private final DecompilerOptions options;
    private String javaVersion;
    private int accessFlags;
    private LabelNode currentLabel;
    private int currentInsn;
    private ASMifier dummyAnnVisitor;
    private DecompiledMethod currMethod;
    private String className;
    private final ClassNode classNode;

    private CommentedASMifierClassVisitor(ClassNode classNode, final DecompilerOptions options, String name, int id) {
        super(Opcodes.ASM5, name, id);
        this.classNode = classNode;
        this.options = options;
        showLines = options.modes.get(BCOConstants.F_SHOW_LINE_INFO);
        showLocals = options.modes.get(BCOConstants.F_SHOW_VARIABLES);
        showStackMap = options.modes.get(BCOConstants.F_SHOW_STACKMAP);
    }

    public CommentedASMifierClassVisitor(ClassNode classNode, final DecompilerOptions options) {
        this(classNode, options, "cw", 0);
    }


    @Override
    protected ASMifier createASMifier(String name1, int id1) {
        CommentedASMifierClassVisitor classVisitor = new CommentedASMifierClassVisitor(
            classNode, options, name1, id1);
        classVisitor.currMethod = currMethod;
        return classVisitor;
    }

    private void addIndex(final int opcode) {
        text.add(new Index(currentLabel, currentInsn++, opcode));
    }

    void setCurrentLabel(LabelNode currentLabel) {
        this.currentLabel = currentLabel;
    }

    private boolean decompilingEntireClass() {
        return options.methodFilter == null && options.fieldFilter == null;
    }

    @Override
    public void visit(int version, int access, String name1, String signature,
        String superName, String[] interfaces) {
        if(decompilingEntireClass()) {
            super.visit(version, access, name1, signature, superName, interfaces);
        }
        this.className = name;
        int major = version & 0xFFFF;
        //int minor = version >>> 16;
        // 1.1 is 45, 1.2 is 46 etc.
        int javaV = major % 44;
        if (javaV > 0 && javaV < 10) {
            javaVersion = "1." + javaV;
        }
        this.accessFlags = access;
    }

    @Override
    public ASMifier visitClassAnnotation(String desc, boolean visible) {
        if (decompilingEntireClass()) {
            return super.visitClassAnnotation(desc, visible);
        }
        return getDummyVisitor();
    }

    @Override
    public void visitClassAttribute(Attribute attr) {
        if (decompilingEntireClass()) {
            super.visitClassAttribute(attr);
        }
    }

    @Override
    public void visitClassEnd() {
        if (decompilingEntireClass()) {
            super.visitClassEnd();
        }
    }

    @Override
    public void visitInnerClass(String name1, String outerName,
        String innerName, int access) {
        if (decompilingEntireClass()) {
            super.visitInnerClass(name1, outerName, innerName, access);
        }
    }

    @Override
    public void visitOuterClass(String owner, String name1, String desc) {
        if (decompilingEntireClass()) {
            super.visitOuterClass(owner, name1, desc);
        }
    }

    @Override
    public void visitSource(String file, String debug) {
        if (decompilingEntireClass()) {
            super.visitSource(file, debug);
        }
    }

    @Override
    public ASMifier visitMethod(int access, String name1, String desc,
        String signature, String[] exceptions) {
        if(options.fieldFilter != null || options.methodFilter != null && !options.methodFilter.equals(name1 + desc)) {
            return getDummyVisitor();
        }

        MethodNode meth = null;
        List<String> exList = Arrays.asList(exceptions);
        for (MethodNode mn : classNode.methods) {
            if(mn.name.equals(name1) && mn.desc.equals(desc) && mn.exceptions.equals(exList)) {
                meth = mn;
                break;
            }
        }
        assert meth != null;

        currMethod = new DecompiledMethod(className, new HashMap<Label, Integer>(), meth, options, access);
        ASMifier textifier = super.visitMethod(access, name1, desc, signature, exceptions);
        TraceMethodVisitor tm = new TraceMethodVisitor(textifier);
        meth.accept(tm);

        Object methodEnd = text.remove(text.size() - 1);
        Object methodtext = text.remove(text.size() - 1);
        currMethod.setText((List) methodtext);
        text.add(currMethod);
        text.add(methodEnd);
        return textifier;
    }

    @Override
    public ASMifier visitField(int access, String name1, String desc,
        String signature, Object value) {
        if (options.methodFilter != null) {
            return getDummyVisitor();
        }
        if (options.fieldFilter != null && !name1.equals(options.fieldFilter)) {
            return getDummyVisitor();
        }
        return super.visitField(access, name1, desc, signature, value);
    }

    @Override
    public void visitFieldInsn(final int opcode, final String owner1,
        final String name1, final String desc) {
        addIndex(opcode);
        super.visitFieldInsn(opcode, owner1, name1, desc);
    }


    @Override
    public void visitFrame(final int type, final int nLocal,
        final Object[] local, final int nStack, final Object[] stack) {
        if (showStackMap) {
            addIndex(-1);
            super.visitFrame(type, nLocal, local, nStack, stack);
        }
    }

    @Override
    public void visitInsn(final int opcode) {
        addIndex(opcode);
        super.visitInsn(opcode);
    }

    @Override
    public void visitIntInsn(int opcode, int operand) {
        addIndex(opcode);
       super.visitIntInsn(opcode, operand);
    }


    @Override
    public void visitJumpInsn(final int opcode, final Label label) {
        addIndex(opcode);
        super.visitJumpInsn(opcode, label);
    }

    @Override
    public void visitLabel(Label label) {
        addIndex(-1);
        super.visitLabel(label);

        InsnList instructions = currMethod.meth.instructions;
        LabelNode currLabel = null;
        for (int i = 0; i < instructions.size(); i++) {
            AbstractInsnNode insnNode = instructions.get(i);
            if(insnNode instanceof LabelNode) {
                LabelNode labelNode = (LabelNode) insnNode;
                if(labelNode.getLabel() == label) {
                    currLabel = labelNode;
                }
            }
        }
        setCurrentLabel(currLabel);
    }

    @Override
    public void visitLdcInsn(final Object cst) {
        addIndex(Opcodes.LDC);
       super.visitLdcInsn(cst);
    }

    @Override
    public void visitInvokeDynamicInsn(String name1, String desc, Handle bsm,
        Object... bsmArgs) {
        addIndex(Opcodes.INVOKEDYNAMIC);
        super.visitInvokeDynamicInsn(name1, desc, bsm, bsmArgs);
    }

    @Override
    public void visitIincInsn(final int var, final int increment) {
        addIndex(Opcodes.IINC);
        super.visitIincInsn(var, increment);
    }

    @Override
    public void visitLineNumber(int line, Label start) {
        if (showLines) {
            addIndex(-1);
            currMethod.addLineNumber(start, Integer.valueOf(line));
            super.visitLineNumber(line, start);
        }
    }

    @Override
    public void visitLookupSwitchInsn(final Label dflt, final int[] keys,
        final Label[] labels) {
        addIndex(Opcodes.LOOKUPSWITCH);
        super.visitLookupSwitchInsn(dflt, keys, labels);
    }

    @Override
    public void visitLocalVariable(String name1, String desc,
        String signature, Label start, Label end, int index) {
        if (showLocals) {
            super.visitLocalVariable(
                name1, desc, signature, start, end, index);
        }
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        super.visitMaxs(maxStack, maxLocals);
    }

    @Override
    public void visitMethodInsn(final int opcode, final String owner,
        final String name1, final String desc) {
        addIndex(opcode);
        super.visitMethodInsn(opcode, owner, name1, desc);
    }

    @Override
    public void visitMultiANewArrayInsn(final String desc, final int dims) {
        addIndex(Opcodes.MULTIANEWARRAY);
        super.visitMultiANewArrayInsn(desc, dims);
    }

    @Override
    public void visitTableSwitchInsn(final int min, final int max,
        final Label dflt, final Label... labels) {
        addIndex(Opcodes.TABLESWITCH);
        super.visitTableSwitchInsn(min, max, dflt, labels);
    }

    @Override
    public void visitTypeInsn(final int opcode, final String desc) {
        addIndex(opcode);
        super.visitTypeInsn(opcode, desc);
    }

    @Override
    public void visitVarInsn(final int opcode, final int var) {
        addIndex(opcode);
        super.visitVarInsn(opcode, var);
    }



    @Override
    public DecompiledClassInfo getClassInfo() {
        return new DecompiledClassInfo(javaVersion, accessFlags);
    }

    private ASMifier getDummyVisitor(){
        if (dummyAnnVisitor == null) {
            dummyAnnVisitor = new ASMifier(Opcodes.ASM5, "", -1) {
                @Override
                public void visitAnnotationEnd() {
                    text.clear();
                }

                @Override
                public void visitClassEnd() {
                    text.clear();
                }

                @Override
                public void visitFieldEnd() {
                    text.clear();
                }

                @Override
                public void visitMethodEnd() {
                    text.clear();
                }
            };
        }
        return dummyAnnVisitor;
    }
}
